/**
 ****************************************************************************
 * Copyright (C) Marcelo F. Ochoa. All rights reserved.                      *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included  with this distribution in *
 * the LICENSE file.                                                         *
 */
 
package com.prism.toolkit.http;

import com.prism.toolkit.Bootstrap;
import com.prism.toolkit.Jxtp;

import java.io.StringBufferInputStream;
import java.io.StringWriter;

import java.lang.reflect.Method;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Timestamp;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Random;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;

import oracle.jdbc.OraclePreparedStatement;
import oracle.jdbc.OracleResultSet;
import oracle.jdbc.driver.OracleDriver;

import oracle.xdb.XMLType;

import org.apache.log4j.Logger;
import oracle.aurora.vm.OracleRuntime;

import oracle.xml.parser.v2.DOMParser;

/**
 * This class encapsulate the access to the tables:
 * create table JSERVER_SESSION_STORE
 *  (
 *   ID VARCHAR2(255),
 *   CreationTime DATE,
 *   MaxInactiveInterval NUMBER,
 *   LastAccessedTime    DATE,
 *   CONSTRAINT JSERVER_SESSION_STORE_PK PRIMARY KEY (ID)
 *  )
 * create table JSERVER_SESSION_OBJECTS
 *  (
 *   IdStore VARCHAR2(255),
 *   AttrName VARCHAR2(255),
 *   AttrValue XMLType,
 *   CONSTRAINT JSERVER_SESSION_OBJECT_PK PRIMARY KEY (IdStore,AttrName),
 *   CONSTRAINT JSERVER_SESSION_OBJECT_FK FOREIGN KEY (IdStore)
 *     REFERENCES JSERVER_SESSION_STORE(ID)
 *  )
 *  Which works as permanent store across multiples stateless calls of
 *  DBPrism Toolkit.
 */
public class JxtpHttpServletSession implements HttpSession {
    Logger log = Logger.getLogger(JxtpHttpServletSession.class);

    private Hashtable store = null;
    private Connection conn = null;
    private String id = null;
    private long creationTime = 0L;
    private long lastAccessedTime = 0L;
    private int maxInactiveInterval = 0;
    private boolean isNew = false;
    
    public JxtpHttpServletSession() {
      log.debug(".JxtpHttpServletSession() called.");
      store = new Hashtable();
      conn = getConnection();
      lastAccessedTime = System.currentTimeMillis();
      try {
        createSession();
      } catch (SQLException s) {
          String errMsg = s.getMessage();
          if (errMsg!=null && errMsg.indexOf("ORA-00942")>0) {
            // ORA-00942: table or view does not exist
            // Try to create a private session store
            try {
              initStore();
              createSession();
            } catch (SQLException e) {
              log.warn(".JxtpHttpServletSession() exception: "+e.getMessage(),e);
              throw new InstantiationError(".JxtpHttpServletSession(): "+e.getMessage());
            }
          } else {
            log.warn(".JxtpHttpServletSession() exception: "+errMsg,s);
            throw new InstantiationError(".JxtpHttpServletSession(): "+errMsg);
          }
      }
    }

    public JxtpHttpServletSession(String idSession) {
      if (log.isDebugEnabled())
        log.debug(".JxtpHttpServletSession("+idSession+") called.");
      store = new Hashtable();
      conn = getConnection();
      lastAccessedTime = System.currentTimeMillis();
      id = idSession;
      try {
        loadSession();
      } catch (SQLException s) {
        String errMsg = s.getMessage();
        if (errMsg!=null && errMsg.indexOf("ORA-00942")>0) {
          // ORA-00942: table or view does not exist
          // Try to create a private session store
          try {
            initStore();
            loadSession();
          } catch (SQLException e) {
            log.warn(".JxtpHttpServletSession() exception: "+e.getMessage(),e);
            throw new InstantiationError(".JxtpHttpServletSession(): "+e.getMessage());
          }
        } else {
          log.warn(".JxtpHttpServletSession("+idSession+") exception: "+s.getMessage(),s);
          throw new InstantiationError(".JxtpHttpServletSession("+idSession+"): "+s.getMessage());
        }
      }
    }

    /**
     * Returns the time when this session was created,
     * measured in milliseconds since midnight January 1, 1970 GMT.
     * @return long
     */
    public long getCreationTime() {
        return creationTime;
    }

    /**
     * Returns a string containing the unique identifier assigned to this session.
     * @return String
     */
    public String getId() {
        return id;
    }

    /**
     * Returns the last time the client sent a request associated with this session,
     * as the number of milliseconds since midnight January 1, 1970 GMT,
     * and marked by the time the container received the request.
     * @return long
     */
    public long getLastAccessedTime() {
        return lastAccessedTime;
    }

    /**
     * Not applicable at this time on DBPrism Toolkit
     * @return null.
     */
    public ServletContext getServletContext() {
        return null;
    }

    /**
     * Specifies the time, in seconds, between client requests before the
     * servlet container will invalidate this session.
     * @param i the inactive interval time.
     */
    public void setMaxInactiveInterval(int i) {
        OraclePreparedStatement stmt = null;
        maxInactiveInterval = i;
        try {
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "update JSERVER_SESSION_STORE set MaxInactiveInterval = ?" +
              "       where id = ?");
            stmt.setInt(1,maxInactiveInterval);
            stmt.setString(2,id);
            stmt.executeUpdate();
            stmt.close();
            stmt = null;
            Jxtp.sendCookie("JSERVER_SESSION",id,new Date(lastAccessedTime+maxInactiveInterval));
        } catch (SQLException s) {
            log.warn(".setMaxInactiveInterval exception: "+s.getMessage(),s);
            throw new InternalError(".setMaxInactiveInterval: "+s.getMessage());
        } finally {
            if (stmt!=null)
              try {
                stmt.close();
              } catch (SQLException sq) {
                  log.warn(".setMaxInactiveInterval exception: "+sq.getMessage(),sq);
                  throw new IllegalArgumentException(".setMaxInactiveInterval: "+sq.getMessage());
              } finally {
                stmt = null;
              }
        }
    }

    /**
     * Returns the maximum time interval, in seconds,
     * that the servlet container will keep this session open between client accesses.
     * @return int
     */
    public int getMaxInactiveInterval() {
        return maxInactiveInterval;
    }

    /**
     * @deprecated As of Version 2.1, this method is deprecated and has no
     * replacement. It will be removed in a future version of the Java Servlet API.
     * @return null
     */
    public HttpSessionContext getSessionContext() {
        return null;
    }

    /**
     * Returns the object bound with the specified name in this session,
     * or null if no object is bound under the name.
     * @param key string with key for looking the XMLType object
     * @return an unmarshalled version of the XMLType object
     */
    public Object getAttribute(String key) {
        return store.get(key);
    }

    /**
     * @deprecated
     * As of Version 2.2, this method is replaced by 
     * @see HttpSession#getAttribute(String).
     * @param string
     * @return null
     */
    public Object getValue(String string) {
        return store.get(string);
    }

    /**
     * Returns an Enumeration of String objects containing the names of 
     * all the objects bound to this session.
     * @return Enumeration
     */
    public Enumeration getAttributeNames() {
        return store.keys();
    }

    /**
     * @deprecated As of Version 2.2, this method is replaced by 
     * @see HttpSession#getAttributeNames()
     * @return String[0]
     */
    public String[] getValueNames() {
        return new String[0];
    }

    /**
     * Binds an object to this session, using the name specified.
     * Unlike Servlet HttpSession implementation this method require key with
     * the form my.pkg.Value where my.pkg is used by JAXBContext.newInstance("my.pkg")
     * before call to the marshall operation of the object value into the XMLType object
     * Also if you change the Java object value instance call again setAttribute
     * to store the new values on the persistent store
     * @param key string for the table JSERVER_SESSION_OBJECTS
     * @param value Object marshalled using Jaxb and stores as XMLType
     */
    public void setAttribute(String key, Object value) {
        OraclePreparedStatement stmt = null;
        XMLType valueXML = null;
        try {
            int lastDot = key.lastIndexOf(".");
            if (lastDot<1)
                throw new IllegalArgumentException(
                   ".setAttribute: invalid key value '"+
                   key+
                   "', must be my.pkg.ClassName");
            String space = key.substring(0,lastDot);
            StringWriter sw = new StringWriter();
            JAXBContext jc = JAXBContext.newInstance(space,Bootstrap.getClassLoader());
            Marshaller m = jc.createMarshaller();
            m.setProperty("jaxb.formatted.output",Boolean.valueOf(true));
            m.marshal(value,sw);
            valueXML = new XMLType(conn,sw.toString());
            if (store.containsKey(key)) {
              stmt = (OraclePreparedStatement)conn.prepareStatement(
                "update JSERVER_SESSION_OBJECTS set" +
                "  AttrValue = ?" +
                "  where" +
                "  IDStore = ? and AttrName = ?");
              stmt.setObject(1,valueXML);
              stmt.setString(2,id);
              stmt.setString(3,key);
            } else {
                stmt = (OraclePreparedStatement)conn.prepareStatement(
                  "insert into JSERVER_SESSION_OBJECTS" +
                  "  (IDStore,AttrName,AttrValue)" +
                  "  values" +
                  "  (?,?,?)");
                stmt.setString(1,id);
                stmt.setString(2,key);
                stmt.setObject(3,valueXML);
            }
            stmt.executeUpdate();
            if (log.isDebugEnabled())
              log.debug(
                ".setAttribute - sucessfull stored:\n"+
                valueXML.getStringVal()+
                "with key: "+key);
            try {
              DOMParser parser = new DOMParser();
              parser.parse(new StringBufferInputStream(valueXML.getStringVal()));
              Class class1 = Class.forName(space+".ObjectFactory", true, Bootstrap.getClassLoader());
              Object obj = class1.newInstance();
              Class aclass[] = {
                  org.w3c.dom.Node.class
              };
              Method method = class1.getMethod("unmarshal", aclass);
              Object aobj[] = {
                parser.getDocument().getDocumentElement()
              };
              Object restoredValue = method.invoke(obj, aobj);
              store.put(key,restoredValue);
            } catch (Exception e) {
              log.warn(".setAttribute - exception restoring object from string",e);  
            } finally {
              valueXML.close();
              valueXML = null;
              stmt.close();
              stmt = null;
            }
        } catch (SQLException s) {
            log.warn(".setAttribute exception: "+s.getMessage(),s);
            throw new InternalError(".setAttribute: "+s.getMessage());
        } catch (JAXBException j) {
            log.warn(".setAttribute exception: "+j.getMessage(),j);
            throw new IllegalArgumentException(".setAttribute: "+j.getMessage());
        } finally {
            if (valueXML!=null) {
                valueXML.close();
                valueXML = null;
            }
            if (stmt!=null)
              try {
                stmt.close();
              } catch (SQLException sq) {
                  log.warn(".setAttribute exception: "+sq.getMessage(),sq);
                  throw new IllegalArgumentException(".setAttribute: "+sq.getMessage());
              } finally {
                stmt = null;
              }
        }
    }

    /**
     * @deprecated As of Version 2.2, this method is replaced by 
     * @see HttpSession#setAttribute(java.lang.String, java.lang.Object)
     * @param string key
     * @param object
     */
    public void putValue(String string, Object object) {
    }

    /**
     * Removes the object bound with the specified name from this session.
     * @param key for the table JSERVER_SESSION_OBJECTS
     */
    public void removeAttribute(String key) {
        OraclePreparedStatement stmt = null;
        try {
            if (store.containsKey(key)) {
              stmt = (OraclePreparedStatement)conn.prepareStatement(
                "delete from JSERVER_SESSION_OBJECTS" +
                "  where IDStore = ? and  AttrName = ?");
              stmt.setString(1,id);
              stmt.setString(2,key);
              stmt.executeUpdate();
              stmt.close();
              stmt = null;
              store.remove(key);
              if (log.isDebugEnabled())
                log.debug(
                  ".removeAttribute - sucessfull removed: "+key);
            }
        } catch (SQLException s) {
            log.warn(".removeAttribute exception: "+s.getMessage(),s);
            throw new InternalError(".removeAttribute: "+s.getMessage());
        } finally {
            if (stmt!=null)
              try {
                stmt.close();
              } catch (SQLException sq) {
                  log.warn(".removeAttribute exception: "+sq.getMessage(),sq);
                  throw new InternalError(".removeAttribute: "+sq.getMessage());
              } finally {
                stmt = null;
              }
        }
    }

    /**
     * @deprecated As of Version 2.2, this method is replaced by
     * @see HttpSession#removeAttribute(java.lang.String)
     * @param string
     */
    public void removeValue(String string) {
    }

    /**
     * Invalidates this session then unbinds any objects bound to it.
     */
    public void invalidate() {
        OraclePreparedStatement stmt = null;
        try {
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "delete from JSERVER_SESSION_OBJECTS" +
              "  where IDStore = ?");
            stmt.setString(1,id);
            stmt.executeUpdate();
            stmt.close();
            stmt = null;
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "delete from JSERVER_SESSION_STORE" +
              "  where ID = ?");
            stmt.setString(1,id);
            stmt.executeUpdate();
            stmt.close();
            stmt = null;
            store.clear();
            if (log.isDebugEnabled())
              log.debug(
                ".invalidate - sucessfull id: "+id);
            id = null;
            Jxtp.removeCookie("JSERVER_SESSION");
        } catch (SQLException s) {
            log.warn(".invalidate exception: "+s.getMessage(),s);
            throw new InternalError(".invalidate: "+s.getMessage());
        } finally {
            if (stmt!=null)
              try {
                stmt.close();
              } catch (SQLException sq) {
                  log.warn(".invalidate exception: "+sq.getMessage(),sq);
                  throw new InternalError(".invalidate: "+sq.getMessage());
              } finally {
                stmt = null;
              }
        }
    }

    /**
     * Returns true if the client does not yet know about the session or if the
     * client chooses not to join the session.
     * @return boolean
     */
    public boolean isNew() {
        return isNew;
    }
    
    public static Connection getConnection() {
        Connection dconn = null;
        try {
            if(System.getProperty("java.vm.name").equals("JServer VM"))
              dconn = (new OracleDriver()).defaultConnection();
            else { // only for testing purpose, it supose to be uses inside the OJVM
              DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
              dconn = DriverManager.getConnection("jdbc:oracle:thin:@p1:1521:dev",
                                                                              "scott",
                                                                              "tiger");
              dconn.setAutoCommit(true);
            }
        } catch(SQLException s) {
          s.printStackTrace(System.err);
          throw new InternalError(
             ".getConnection: Can't get defaultConnection "+s.getMessage());
        }
        return dconn;
    }
    
    private void initStore() throws SQLException {
        OraclePreparedStatement stmt = null;
        try {
          stmt = (OraclePreparedStatement)conn.prepareStatement(
             "create table JSERVER_SESSION_STORE\n" + 
             "(\n" + 
             "ID VARCHAR2(255),\n" + 
             "CreationTime DATE,\n" + 
             "MaxInactiveInterval NUMBER,\n" + 
             "LastAccessedTime    DATE,\n" + 
             "CONSTRAINT JSERVER_SESSION_STORE_PK PRIMARY KEY (ID)\n" + 
             ")");
          stmt.executeUpdate();
          stmt.close();
          stmt = (OraclePreparedStatement)conn.prepareStatement(
             "create table JSERVER_SESSION_OBJECTS\n" + 
             "(\n" + 
             "IdStore VARCHAR2(255),\n" + 
             "AttrName VARCHAR2(255),\n" + 
             "AttrValue XMLType,\n" + 
             "CONSTRAINT JSERVER_SESSION_OBJECT_PK PRIMARY KEY (IdStore,AttrName),\n" + 
             "CONSTRAINT JSERVER_SESSION_OBJECT_FK FOREIGN KEY (IdStore)\n" + 
             "      REFERENCES JSERVER_SESSION_STORE(ID)\n" + 
             ")");
          stmt.executeUpdate();
          stmt.close();
          stmt = null;
        } catch (SQLException s) {
          log.warn(".JxtpHttpServletSession() exception: "+s.getMessage(),s);
          throw s;
        } finally {
          if (stmt!=null) {
            stmt.close();
            stmt = null;
          }
        }
    }

    private void createSession() throws SQLException {
        OraclePreparedStatement stmt = null;
        try {
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "insert into JSERVER_SESSION_STORE" +
              "    (ID,CreationTime,MaxInactiveInterval,LastAccessedTime)" +
              "    values" +
              "    (?,?,?,?)");
            id = rndValue();
            creationTime = lastAccessedTime;
            maxInactiveInterval = Integer.MAX_VALUE;
            stmt.setString(1,id);
            stmt.setTimestamp(2,new Timestamp(creationTime));
            stmt.setInt(3,maxInactiveInterval);
            stmt.setTimestamp(4,new Timestamp(lastAccessedTime));
            stmt.executeUpdate();
            stmt.close();
            stmt = null;
            Jxtp.sendCookie("JSERVER_SESSION",id,new Date(creationTime+maxInactiveInterval));
            isNew = true;
            if (log.isDebugEnabled())
              log.debug(".createSession - new session created with ID: "+id);
        } catch (SQLException s) {
            log.warn(".createSession() exception: "+s.getMessage(),s);
            throw new SQLException(
               ".createSession: Can't create a new session "+s.getMessage());
        } finally {
            if (stmt!=null) {
              stmt.close();
              stmt = null;
            }
        }        
    }
    
    private void loadSessionValues() throws SQLException {
        OraclePreparedStatement stmt = null;
        OracleResultSet rset = null;
        try {
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "select CreationTime,MaxInactiveInterval from JSERVER_SESSION_STORE" +
              "       where id = ?");
            stmt.setString(1,id);
            rset = (OracleResultSet)stmt.executeQuery();
            if (rset.next()) {
              creationTime = rset.getTimestamp(1).getTime();
              maxInactiveInterval = rset.getInt(2);
            } else
                throw new IllegalStateException(
                  ".loadSessionValues: Can't session store values");
            rset.close();
            rset = null;
            stmt.close();
            stmt = null;
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "select AttrName,AttrValue from JSERVER_SESSION_OBJECTS" +
              "       where idstore = ?");
            stmt.setString(1,id);
            rset = (OracleResultSet)stmt.executeQuery();
            while(rset.next()) {
              String key = rset.getString(1);
              XMLType valueXML = (XMLType)rset.getObject(2);
              int lastDot = key.lastIndexOf(".");
              if (lastDot<1)
                  throw new IllegalArgumentException(
                     ".loadSessionValues: invalid key value '"+
                     key+
                     "', must be my.pkg.Value");
              String space = key.substring(0,lastDot);
              if (log.isDebugEnabled())
                log.debug(
                  ".loadSessionValues: attemting to unmarshall:\n"+
                  valueXML.getStringVal()+
                  " with space: "+space);
              DOMParser parser = new DOMParser();
              parser.parse(new StringBufferInputStream(valueXML.getStringVal()));
              //JAXBContext jc = JAXBContext.newInstance(space,OracleRuntime.getCallerClass().getClassLoader());
              //Create an unmarshaller instance
              //Unmarshaller u = jc.createUnmarshaller();
              //u.setValidating(true);
              //Object value = u.unmarshal(parser.getDocument().getDocumentElement());
              //Object value = u.unmarshal(new StringBufferInputStream(valueXML.getStringVal()));
              valueXML.close();
              valueXML = null;
              Class class1 = Class.forName(space+".ObjectFactory", true, Bootstrap.getClassLoader());
              Object obj = class1.newInstance();
              Class aclass[] = {
                    org.w3c.dom.Node.class
              };
              Method method = class1.getMethod("unmarshal", aclass);
              Object aobj[] = {
                parser.getDocument().getDocumentElement()
              };
              Object value = method.invoke(obj, aobj);
              store.put(key,value);
              if (log.isDebugEnabled())
                log.debug(
                  ".loadSessionValues - sucessfull loaded:\n"+
                  value+" with key: "+key);
            }
            rset.close();
            rset = null;
            stmt.close();
            stmt = null;
        } catch (Exception s) {
          log.warn(".loadSessionValues exception: "+s.getMessage(),s);
          throw new SQLException(
            ".loadSessionValues: Can't load session values "+s.getMessage());
        } finally {
            if (rset!=null) {
              rset.close();
              rset = null;
            }
            if (stmt!=null) {
              stmt.close();
              stmt = null;
            }
        }
    }
    
    private void loadSession() throws SQLException {
        OraclePreparedStatement stmt = null;
        try {
            stmt = (OraclePreparedStatement)conn.prepareStatement(
              "update JSERVER_SESSION_STORE set LastAccessedTime=?" +
              "       where id = ?");
            stmt.setTimestamp(1,new Timestamp(lastAccessedTime));
            stmt.setString(2,id);
            int affectedRows = stmt.executeUpdate();
            stmt.close();
            stmt = null;
            if (affectedRows==0) { // there is no session row, create a new one
              Jxtp.removeCookie("JSERVER_SESSION");
              throw new InstantiationError(".loadSession: Invalid JSERVER_SESSION Cookie");
            }
            loadSessionValues();
            Jxtp.sendCookie("JSERVER_SESSION",id,new Date(lastAccessedTime+maxInactiveInterval));
        } catch (SQLException s) {
            log.warn(".loadSession exception: "+s.getMessage(),s);
            throw new SQLException(
              ".loadSession: Can't load session values "+s.getMessage());
        } finally {
            if (stmt!=null) {
              stmt.close();
              stmt = null;
            }
        }
    }
    
    /**
     * @return a very long string random value for validation support
     */
    public static String rndValue() {
        byte[] bytes = new byte[27];
        Random random = new java.util.Random();
        random.setSeed(System.currentTimeMillis());
        random.nextBytes(bytes);

        // Render the result as a String of hexadecimal digits
        StringBuffer result = new StringBuffer();

        for (int i = 0; i < bytes.length; i++) {
            byte b1 = (byte) ((bytes[i] & 0xf0) >> 4);
            byte b2 = (byte) (bytes[i] & 0x0f);

            if (b1 < 10) {
                result.append((char) ('0' + b1));
            } else {
                result.append((char) ('A' + (b1 - 10)));
            }

            if (b2 < 10) {
                result.append((char) ('0' + b2));
            } else {
                result.append((char) ('A' + (b2 - 10)));
            }
        }

        return (result.toString());
    }

}
